import functools
import json
import sys
import tempfile
import uuid
from datetime import timedelta
from decimal import Decimal
from typing import Optional
from unittest import mock

import pytest
from django import __version__ as DJANGO_VERSION
from django.core.files.base import ContentFile
from django.core.files.storage import FileSystemStorage
from django.db import models
from django.urls import reverse
from django.utils.functional import cached_property
from rest_framework import serializers, viewsets
from rest_framework.routers import SimpleRouter
from rest_framework.test import APIClient

from drf_spectacular.generators import SchemaGenerator
from tests import assert_equal, assert_schema, build_absolute_file_path

if sys.version_info >= (3, 8):
    functools_cached_property = functools.cached_property
else:
    # We re-use Django's cached_property when it's not available to
    # keep tests unified across Python versions.
    functools_cached_property = cached_property


fs = FileSystemStorage(location=tempfile.gettempdir())


class Aux(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    field_foreign = models.ForeignKey('Aux', null=True, on_delete=models.CASCADE)
    url = models.URLField(unique=True, help_text="URL identifier for Aux")


class AuxSerializer(serializers.ModelSerializer):
    """ description for aux object """
    class Meta:
        fields = '__all__'
        model = Aux


class SubObject:
    def __init__(self, instance):
        self._instance = instance

    @property
    def calculated(self) -> int:
        """ My calculated property """
        return self._instance.field_int

    @property
    def nested(self) -> 'SubObject':
        return self

    @property
    def model_instance(self) -> 'AllFields':
        return self._instance

    @property
    def optional_int(self) -> Optional[int]:
        return 1


class AllFields(models.Model):
    # basics
    field_int = models.IntegerField()
    field_float = models.FloatField()
    field_bool = models.BooleanField()
    field_char = models.CharField(max_length=100)
    field_text = models.TextField(verbose_name='a text field')
    # special
    field_slug = models.SlugField()
    field_email = models.EmailField()
    field_uuid = models.UUIDField()
    field_url = models.URLField()
    field_ip_generic = models.GenericIPAddressField(protocol='ipv6')
    field_decimal = models.DecimalField(max_digits=6, decimal_places=3)
    field_file = models.FileField(storage=fs)
    field_img = models.ImageField(storage=fs)
    field_date = models.DateField()
    field_datetime = models.DateTimeField()
    field_bigint = models.BigIntegerField()
    field_smallint = models.SmallIntegerField()
    field_posint = models.PositiveIntegerField()
    field_possmallint = models.PositiveSmallIntegerField()
    if DJANGO_VERSION > '3.1':
        field_nullbool = models.BooleanField(null=True)
    else:
        field_nullbool = models.NullBooleanField()
    field_time = models.TimeField()
    field_duration = models.DurationField()
    field_binary = models.BinaryField()

    # relations
    field_foreign = models.ForeignKey(
        Aux, on_delete=models.CASCADE, help_text='main aux object', related_name='ff'
    )
    field_m2m = models.ManyToManyField(
        Aux, help_text='set of related aux objects', related_name='fm'
    )
    field_o2o = models.OneToOneField(
        Aux, on_delete=models.CASCADE, help_text='bound aux object', related_name='fo'
    )
    # overrides
    field_regex = models.CharField(max_length=50)
    field_bool_override = models.BooleanField()

    if DJANGO_VERSION >= '3.1':
        field_json = models.JSONField()
    else:
        @property
        def field_json(self):  # type: ignore[misc]
            return {'A': 1, 'B': 2}

    @property
    def field_model_property_float(self) -> float:
        return 1.337

    @cached_property
    def field_model_cached_property_float(self) -> float:
        return 1.337

    @functools_cached_property
    def field_model_py_cached_property_float(self) -> float:
        return 1.337

    @property
    def field_list(self):
        return [1.1, 2.2, 3.3]

    @property
    def field_list_object(self):
        return self.field_m2m.all()

    def model_function_basic(self) -> bool:
        return True

    def model_function_model(self) -> Aux:
        return self.field_foreign

    @property
    def model_property_model(self) -> Aux:
        return self.field_foreign

    @property
    def sub_object(self) -> SubObject:
        return SubObject(self)

    @cached_property
    def sub_object_cached(self) -> SubObject:
        return SubObject(self)

    @functools_cached_property
    def sub_object_py_cached(self) -> SubObject:
        return SubObject(self)

    @property
    def optional_sub_object(self) -> Optional[SubObject]:
        return SubObject(self)


class AllFieldsSerializer(serializers.ModelSerializer):
    field_decimal_uncoerced = serializers.DecimalField(
        source='field_decimal',
        max_digits=6,
        decimal_places=3,
        coerce_to_string=False
    )
    field_method_float = serializers.SerializerMethodField()

    def get_field_method_float(self, obj) -> float:
        return 1.3456

    field_method_object = serializers.SerializerMethodField()

    def get_field_method_object(self, obj) -> dict:
        return {'key': 'value'}

    field_regex = serializers.RegexField(r'^[a-zA-z0-9]{10}\-[a-z]', label='A regex field')

    field_hidden = serializers.HiddenField(default='')

    # composite fields
    field_list = serializers.ListField(
        child=serializers.FloatField(), min_length=3, max_length=100,
    )
    field_list_serializer = serializers.ListField(
        child=AuxSerializer(),
        source='field_list_object',
    )

    # extra related fields
    field_related_slug = serializers.SlugRelatedField(
        read_only=True, source='field_foreign', slug_field='url',
    )  # type: ignore
    field_related_slug_queryset = serializers.SlugRelatedField(
        source='field_foreign', slug_field='url', queryset=Aux.objects.all()
    )
    field_related_slug_many = serializers.SlugRelatedField(
        many=True, read_only=True, source='field_m2m', slug_field='url',
    )  # type: ignore
    field_related_string = serializers.StringRelatedField(
        source='field_foreign'
    )  # type: ignore
    field_related_hyperlink = serializers.HyperlinkedRelatedField(
        read_only=True, source='field_foreign', view_name='aux-detail'
    )  # type: ignore
    field_identity_hyperlink = serializers.HyperlinkedIdentityField(
        read_only=True, view_name='allfields-detail'
    )

    # read only - model traversal
    field_read_only_nav_uuid = serializers.ReadOnlyField(source='field_foreign.id')
    field_read_only_nav_uuid_3steps = serializers.ReadOnlyField(
        source='field_foreign.field_foreign.field_foreign.id',
        allow_null=True,  # force field output even if traversal fails
    )
    field_read_only_model_function_basic = serializers.ReadOnlyField(source='model_function_basic')
    field_read_only_model_function_model = serializers.ReadOnlyField(source='model_function_model.id')
    field_read_only_model_property_model = serializers.ReadOnlyField(source='model_property_model.id')

    # override default writable bool field with readonly
    field_bool_override = serializers.ReadOnlyField()

    field_model_property_float = serializers.ReadOnlyField()

    field_model_cached_property_float = serializers.ReadOnlyField()

    field_model_py_cached_property_float = serializers.ReadOnlyField()

    field_dict_int = serializers.DictField(
        child=serializers.IntegerField(),
        source='field_json',
    )

    # there is a JSON model field for django>=3.1 that would be placed automatically. for <=3.1 we
    # need to set the field explicitly. defined here for both cases to have consistent ordering.
    field_json = serializers.JSONField()

    # traversal of non-model types of complex object
    field_sub_object_calculated = serializers.ReadOnlyField(source='sub_object.calculated')
    field_sub_object_nested_calculated = serializers.ReadOnlyField(source='sub_object.nested.calculated')
    field_sub_object_model_int = serializers.ReadOnlyField(source='sub_object.model_instance.field_int')

    field_sub_object_cached_calculated = serializers.ReadOnlyField(source='sub_object_cached.calculated')
    field_sub_object_cached_nested_calculated = serializers.ReadOnlyField(source='sub_object_cached.nested.calculated')
    field_sub_object_cached_model_int = serializers.ReadOnlyField(source='sub_object_cached.model_instance.field_int')

    field_sub_object_py_cached_calculated = serializers.ReadOnlyField(source='sub_object_py_cached.calculated')
    field_sub_object_py_cached_nested_calculated = serializers.ReadOnlyField(
        source='sub_object_py_cached.nested.calculated',
    )
    field_sub_object_py_cached_model_int = serializers.ReadOnlyField(
        source='sub_object_py_cached.model_instance.field_int',
    )

    # typing.Optional
    field_optional_sub_object_calculated = serializers.ReadOnlyField(
        source='optional_sub_object.calculated',
        allow_null=True,
    )
    field_sub_object_optional_int = serializers.ReadOnlyField(
        source='sub_object.optional_int',
        allow_null=True,
    )

    class Meta:
        fields = '__all__'
        model = AllFields


class AllFieldsModelViewset(viewsets.ReadOnlyModelViewSet):
    serializer_class = AllFieldsSerializer
    queryset = AllFields.objects.all()

    def retrieve(self, request, *args, **kwargs):
        return super().retrieve(request, *args, **kwargs)


class AuxModelViewset(viewsets.ReadOnlyModelViewSet):
    serializer_class = AuxSerializer
    queryset = Aux.objects.all()


router = SimpleRouter()
router.register('allfields', AllFieldsModelViewset)
router.register('aux', AuxModelViewset)
urlpatterns = router.urls


@pytest.mark.urls(__name__)
def test_fields(no_warnings, django_transforms):
    assert_schema(
        SchemaGenerator().get_schema(request=None, public=True),
        'tests/test_fields.yml',
        transforms=django_transforms,
    )


@pytest.mark.urls(__name__)
@mock.patch('drf_spectacular.settings.spectacular_settings.OAS_VERSION', '3.1.0')
def test_fields_oas_3_1(no_warnings, django_transforms):
    assert_schema(
        SchemaGenerator().get_schema(request=None, public=True),
        'tests/test_fields_oas_3_1.yml',
        transforms=django_transforms,
    )


@pytest.mark.urls(__name__)
@pytest.mark.django_db
def test_model_setup_is_valid():
    aux = Aux(
        id='0ac6930d-87f4-40e8-8242-10a3ed31a335',
        url='https://xkcd.com'
    )
    aux.save()

    m = AllFields(
        # basics
        field_int=1,
        field_float=1.25,
        field_bool=True,
        field_char='char',
        field_text='text',
        # special
        field_slug='all_fields',
        field_email='test@example.com',
        field_uuid='00000000-00000000-00000000-00000000',
        field_url='https://github.com/tfranzel/drf-spectacular',
        field_ip_generic='2001:db8::8a2e:370:7334',
        field_decimal=Decimal('-666.333'),
        field_file=None,
        field_img=None,  # TODO fill with data below
        field_date='2021-09-09',
        field_datetime='2021-09-09T10:15:26.049862',
        field_bigint=11111111111111,
        field_smallint=111111,
        field_posint=123,
        field_possmallint=1,
        field_nullbool=None,
        field_time='00:05:23.283',
        field_duration=timedelta(seconds=10),
        field_binary=b'\xce\x9c\xce\xb9\xce\xb1',
        # relations
        field_foreign=aux,
        field_o2o=aux,
        # overrides
        field_regex='12345asdfg-a',
        field_bool_override=True,
    )
    if DJANGO_VERSION >= '3.1':
        m.field_json = {'A': 1, 'B': 2}
    m.field_file.save('hello.txt', ContentFile("hello world"), save=True)
    m.save()
    m.field_m2m.add(aux)

    response = APIClient().get(reverse('allfields-detail', args=(m.pk,)))
    assert response.status_code == 200

    with open(build_absolute_file_path('tests/test_fields_response.json')) as fh:
        expected = json.load(fh)

    if DJANGO_VERSION < '3':
        expected['field_file'] = f'http://testserver/allfields/1/{m.field_file.name}'
    else:
        expected['field_file'] = f'http://testserver/{m.field_file.name}'

    if DJANGO_VERSION >= '5':
        expected['field_datetime'] = '2021-09-09T10:15:26.049862-05:00'

    assert_equal(json.loads(response.content), expected)
